/* network.c equivalent for towerhack server */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include "network.h"
#include "bailout.h"
#include "main.h"

#if TOWERHACK_DEBUG
#define DISC_TIMELIMIT (FPS*20)
#else
#define DISC_TIMELIMIT (FPS*6)
#endif
#define MSGLOG_SIZE 64
#define MAX_REPEAT_AT_ONCE 3

int sockhdl; /* socket handle */
int seq [MAX_CLIENTS]; /* sequence numbers for outgoing packets */
int seq_in [MAX_CLIENTS]; /* sequence numbers for incoming packets */
StoCmessage msglog [MAX_CLIENTS] [MSGLOG_SIZE];

int connected = 0;

/* All the clients which are currently connected and their players */
Client peer [MAX_CLIENTS];
Player player [MAX_CLIENTS]; // player[x] does not necessarily belong to peer[x]
int owner [MAX_CLIENTS]; // but player[x] belongs to client[owner[x]]

/* Funny callbacks! */
void (*notify_clientjoin) (int index) = 0;
void (*notify_playerjoin) (int index) = 0;
void (*notify_clientleave) (int index) = 0;
void (*notify_playerleave) (int index) = 0;

/* initializes the socket. param is ignored */
void network_init (const char* servername)
{
	#if TOWERHACK_DEBUG
	fprintf (stderr, "[Debug] network_init (%s), message sizes are: StoC: %zd CtoS: %zd\n", servername, sizeof(StoCmessage), sizeof(CtoSmessage));
	#endif
	
	if (!connected) {
		int error;
		struct addrinfo hints, *bind_addr;
		memset(&hints, 0, sizeof hints);
		hints.ai_family = AF_UNSPEC; // set to AF_INET to force IPv4
		hints.ai_socktype = SOCK_DGRAM;
		hints.ai_flags = AI_PASSIVE; // use my IP

		if ((error = getaddrinfo(NULL, CTXSTR(SERVER_PORT), &hints, &bind_addr)) != 0) {
			bailout("ss", "getaddrinfo: ", gai_strerror(error));
		}
		
		sockhdl = socket(AF_INET, SOCK_DGRAM, 0);
		if (sockhdl < 0) bailout ("sd", "could not create server dgram socket, errno=", errno);
		/*
		struct sockaddr_in bind_addr;
		memset (&bind_addr, 0, sizeof(bind_addr));
		bind_addr.sin_family = AF_INET;
		bind_addr.sin_addr.s_addr = INADDR_ANY;
		bind_addr.sin_port = SERVER_PORT; */
		error = bind(sockhdl, (struct sockaddr *) bind_addr->ai_addr, bind_addr->ai_addrlen);
		if (error != 0) bailout ("s", "could not bind socket");
		
		freeaddrinfo (bind_addr);
		memset (seq, 0, sizeof(seq));
		memset (seq_in, 0, sizeof(seq_in));
		memset (msglog, 0, sizeof(msglog));
		memset (peer, 0, sizeof(peer));
		memset (player, 0, sizeof(player));
		memset (owner, 0, sizeof(owner));
		
		connected = 1;
	}
}

void remove_client (int index)
{
	int i;
	StoCmessage message;
	memset (&message, 0, sizeof (message));
	
	#if TOWERHACK_DEBUG
	fprintf (stderr, "[Debug] remove_client (index %d)\n", index);
	#endif
	
	if (notify_clientleave != 0) notify_clientleave (index);
	
	for (i = 0; i < MAX_CLIENTS; i++) {
		if (owner[i] == index) {
			memset (&player[i], 0, sizeof (Player));
		}
	}
	memset (&peer[index], 0, sizeof (Client));
	
	message.type = StoC_LOBBY_CLIENTLEAVE;
	message.clientidx = index;
	send_StoCmessage (&message, -1);
}

void kick_client (int index)
{
	StoCmessage kickmsg;
	memset(&kickmsg, 0, sizeof(kickmsg));
	kickmsg.type = StoC_KICK;
	send_StoCmessage (&kickmsg, index);
	remove_client (index);
}

void network_update ()
{
	int active_clients = 0;
	for (int i = 0; i < MAX_CLIENTS; i++) {
		if (*peer[i].nickname != 0) {
			peer[i].disc_countdown --;
			if (peer[i].disc_countdown <= 0) kick_client (i);
			else active_clients++;
		}
	}
	if ((active_clients < 1) && (global_state != STATE_LOBBY)) 
		change_state (STATE_LOBBY); // wait for new clients in lobby, not ingame
}

void network_cleanup ()
{
	if (shutdown(sockhdl, SHUT_RDWR) != 0)
		bailout ("s", "could not shutdown socket");
	
	if (close(sockhdl) != 0)
		bailout ("s", "could not close socket");
	
	connected = 0;
}

/* The following 4 functions are called when the corresponding message is received from the server.
  They go on to call the callback functions defined in network.h.
  Join callbacks are called after the join, leave callbacks BEFORE the leave. So the callbacks have
  access to the data (nicknames etc.) either way.
*/
void insert_client (char* nickname, int uid, struct sockaddr_storage* address, int addr_size)
{
	int i;
	StoCmessage message;
	memset (&message, 0, sizeof (message));
	
	#if TOWERHACK_DEBUG
	fprintf (stderr, "[Debug] insert_client (nickname %s, <address>, <addr_size>)\n", nickname);
	#endif
	
	for (i = 0; i < MAX_CLIENTS; i++) {
		if (*peer[i].nickname == 0) {
			seq[i] = 1;
			seq_in[i] = 0;
			
			message.type = StoC_LOBBY_NEWCLIENT;
			message.clientidx = i;
			strcpy (message.nickname, nickname); 
			send_StoCmessage (&message, -1);
			
			strcpy (peer[i].nickname, nickname);
			memcpy (&peer[i].address, address, addr_size);
			peer[i].uid = uid;
			peer[i].disc_countdown = DISC_TIMELIMIT;
			
			message.type = StoC_ALLINFO;
			message.allinfo.state = global_state; // global game state
			memcpy (message.allinfo.peer, peer, sizeof(peer));
			memcpy (message.allinfo.player, player, sizeof(player));
			memcpy (message.allinfo.owner, owner, sizeof(owner));
			send_StoCmessage (&message, i);
			
			if (notify_clientjoin != 0) notify_clientjoin (i);
			return;
		}
	}
}

void insert_player (int owneridx, char* nickname, int character)
{
	int i;
	StoCmessage message;
	memset (&message, 0, sizeof (message));
	
	#if TOWERHACK_DEBUG
	fprintf (stderr, "[Debug] insert_player (owner %d, nickname %s, character %d)\n", owneridx, nickname, character);
	#endif
	
	for (i = 0; i < MAX_CLIENTS; i++) {
		if (*player[i].data.nickname == 0) {
			message.type = StoC_LOBBY_NEWPLAYER;
			message.owner = owneridx;
			message.playeridx = i;
			strcpy (message.player.data.nickname, nickname);
			message.player.data.character = character;
			send_StoCmessage (&message, -1);
			
			owner[i] = owneridx;
			strcpy (player[i].data.nickname, nickname);
			player[i].data.character = character;
			if (notify_playerjoin != 0) notify_playerjoin (i);
			return;
		}
	}
}

void remove_player (int index)
{
	StoCmessage message;
	memset (&message, 0, sizeof (message));

	#if TOWERHACK_DEBUG
	fprintf (stderr, "[Debug] remove_player (index %d)\n", index);
	#endif
	
	if (notify_playerleave != 0) notify_playerleave (index);
	memset (&player[index], 0, sizeof (Player));
	
	message.type = StoC_LOBBY_PLAYERLEAVE;
	message.playeridx = index;
	send_StoCmessage (&message, -1);
}

/* ALL SEND AND RECEIVE METHODS transform the structs that are sent back and
  forth into a TEXT BASED human readable protocol and parse it. */

/* Does the same as send_StoCmessage but with explicitly specified seq # and no broadcast option. */
void send_StoCmessage_seq (StoCmessage* msg, int recipient, int* seq)
{
	if (*peer[recipient].nickname == 0) return; // no client there

	char buffer [BUFSIZE];
	memset (buffer, 0, sizeof(buffer));
	int written = 0;
	
	switch (msg->type) {
	
	case StoC_PULSE:
		sprintf (buffer, "0 PULSE\n");
		break;
	
	case StoC_KICK:
		sprintf (buffer, "0 KICK\n");
		break;
	
	case StoC_NOTIFY:
		sprintf (buffer, "%d NOTIFY \"%s\"\n", *seq, msg->line);
		break;
	
	case StoC_CHAT:
		sprintf (buffer, "%d CHAT \"%s\"\n", *seq, msg->line);
		break;
	
	case StoC_ALLINFO:
		written = sprintf (buffer, "%d ALLINFO %d ", *seq, global_state);
		for (int i = 0; i < MAX_CLIENTS; i++) {
			if (*peer[i].nickname != 0) written += sprintf (buffer+written, "\"%s\" %d %d ", peer[i].nickname, peer[i].ready, peer[i].uid);
			else written += sprintf (buffer+written, "\"\" 0 ");
		}
		for (int i = 0; i < MAX_CLIENTS; i++) {
			if (*player[i].data.nickname != 0) written += sprintf (buffer+written, "\"%s\" %d ", player[i].data.nickname, player[i].data.character);
			else written += sprintf (buffer+written, "\"\" 0 ");
		}
		for (int i = 0; i < MAX_CLIENTS; i++) {
			written += sprintf (buffer+written, "%d ", owner[i]);
		}
		sprintf (buffer+written, "\n");
		break;
	
	case StoC_LOBBY_NEWCLIENT:
		sprintf (buffer, "%d LOBBY_NEWCLIENT %d \"%s\"\n", *seq, msg->clientidx, msg->nickname);
		break;
	
	case StoC_LOBBY_NEWPLAYER:
		sprintf (buffer, "%d LOBBY_NEWPLAYER %d %d \"%s\" %d\n", *seq, msg->owner, msg->playeridx, msg->player.data.nickname, msg->player.data.character);
		break;
	
	case StoC_LOBBY_PLAYERLEAVE:
		sprintf (buffer, "%d LOBBY_PLAYERLEAVE %d\n", *seq, msg->playeridx);
		break;
	
	case StoC_LOBBY_CLIENTLEAVE:
		sprintf (buffer, "%d LOBBY_CLIENTLEAVE %d\n", *seq, msg->clientidx);
		break;
	
	case StoC_LOBBY_SEEDGAME:
		sprintf (buffer, "%d LOBBY_SEEDGAME %lu\n", *seq, msg->seed);
		break;
	
	case StoC_LOBBY_CLIENTREADY:
		sprintf (buffer, "%d LOBBY_CLIENTREADY %d\n", *seq, msg->clientidx);
		break;
	
	case StoC_LOBBY_CLIENTNOTREADY:
		sprintf (buffer, "%d LOBBY_CLIENTNOTREADY %d\n", *seq, msg->clientidx);
		break;
	
	case StoC_LOBBY_STARTGAME:
		sprintf (buffer, "%d LOBBY_STARTGAME\n", *seq);
		break;

	case StoC_INGAME_ACTION:
		written = sprintf (buffer, "%d INGAME_ACTION %d %d %d %d %d %d %d", 
			*seq, msg->action.type, msg->action.actor, msg->action.start_time,
			msg->action.location.x, msg->action.location.y, msg->action.location.z,
			msg->action.dir);
		
		switch (msg->action.type) { // append action type specific data
			case ACTION_ATTACK_CLOSE:
				sprintf (buffer+written, " %d\n", msg->action.cattack->weapon);
				break;
			
			case ACTION_ATTACK_RANGE:
				sprintf (buffer+written, " %d\n", msg->action.rattack->weapon);
				break;
		
			default: sprintf (buffer+written, "\n"); break;
		}
		break;

	case StoC_INGAME_DAMAGE:
		written = sprintf (buffer,
			"%d INGAME_DAMAGE %d %d %d %d %d %d %d %d %d\n", *seq,
			msg->damage.time,     msg->damage.where.x,
			msg->damage.where.y,  msg->damage.where.z,
			msg->damage.attacker, msg->damage.victim,
			msg->damage.weapon,   msg->damage.damage,
			msg->damage.hp);
		break;

	case StoC_INGAME_DIE:
		written = sprintf (buffer,
			"%d INGAME_DIE %d %d %d %d %d %d %d %d %d\n", *seq,
			msg->damage.time,     msg->damage.where.x,
			msg->damage.where.y,  msg->damage.where.z,
			msg->damage.attacker, msg->damage.victim,
			msg->damage.weapon,   msg->damage.damage,
			msg->damage.hp);
		break;
			
	case StoC_INGAME_SETPLAYER:
		/* TODO: Need only player.skill and player.inv ? */
		sprintf (buffer, "%d INGAME_SETPLAYER %d %d %d %d %d %d %d %d\n", *seq, msg->who, msg->level, msg->hp, msg->inv.slot[0], msg->inv.slot[1], msg->inv.slot[2], msg->inv.slot[3], msg->inv.slot[4]);
		break;
	
	case StoC_INGAME_OVER:
		sprintf (buffer, "%d INGAME_OVER %d\n", *seq, msg->winner);
		break;
	
	default:
		bailout("sds", "cannot send_StoCmessage type ", msg->type, " (unknown)");
		break;
	
	}
	
	// decide whether to increment seq
	switch (msg->type) {
		case StoC_PULSE:
		case StoC_KICK:
			break;
		default:
			memcpy (&msglog[recipient][ (*seq) % MSGLOG_SIZE ], msg, sizeof(StoCmessage)); // store message
			(*seq)++;
			break;
	}
	
	#if TOWERHACK_DEBUG
	if (msg->type != StoC_PULSE) fprintf (stderr, "[Debug] SEND to %d: %s", recipient, buffer);
	#endif
	
	int error = sendto (sockhdl, buffer, sizeof(buffer), 0, (struct sockaddr* ) &peer[recipient].address, sizeof(struct sockaddr_storage));
	if (error != sizeof(buffer)) bailout ("sd", "error on send data to client ", recipient);
}

/* Send server to client message. Recipient == -1 -> broadcast to all clients. */
void send_StoCmessage (StoCmessage* msg, int recipient)
{
	if (recipient == -1) { // broadcast
		for (int i = 0; i < MAX_CLIENTS; i++) send_StoCmessage (msg, i);
	} else { // address only 1 client
		/* test REPEAT packets
	#if TOWERHACK_DEBUG
		if ((msg->type != StoC_PULSE) && ((seq[recipient] % 50) == 49)) {
			fprintf (stderr, "[Debug] Dropping packet %d on purpose\n", seq[recipient]);
			memcpy (&msglog[recipient][ seq[recipient] % MSGLOG_SIZE ], msg, sizeof(StoCmessage)); // store message
			seq[recipient]++;
			return;
		}
	#endif */
		send_StoCmessage_seq (msg, recipient, &seq[recipient]);
	}
}

CtoSmsgType parsetype (char** token)
{
	char* typestr = *token;
	while (isspace(*typestr)) typestr++;
	
	#define TRYPARSE(STR) if (strncmp(typestr, #STR, sizeof(#STR)-1) == 0) { *token = typestr + sizeof(#STR)-1; return CtoS_##STR; }
	
	TRYPARSE(PULSE) else
	TRYPARSE(HELLO) else
	TRYPARSE(REPEAT) else
	TRYPARSE(CHAT) else
	TRYPARSE(LOBBY_ADDPLAYER) else
	TRYPARSE(LOBBY_SETPLAYERDATA) else
	TRYPARSE(LOBBY_REMOVEPLAYER) else
	TRYPARSE(LOBBY_READY) else
	TRYPARSE(LOBBY_NOTREADY) else
	TRYPARSE(INGAME_ACTION) else
	TRYPARSE(INGAME_SETINVENTORY) else
		#if TOWERHACK_DEBUG
		fprintf (stderr, "[Debug] Failed to parsetype \"%s\".\n", typestr);
		#endif
	return CtoS_ILLEGALMSG;
}

void parsestring (char* to, char** from)
{
	char* begin = strchr(*from, '"')+1;
	char* end = strchr(begin, '"');
	strncpy(to, begin, end-begin);
	*(to+(end-begin)) = '\0';
	*from = end+1;
}

int client_by_uid (int uid)
{
	int i;
	for (i = 0; i < MAX_CLIENTS; i++)
		if (peer[i].uid == uid) return i;
	
	return -1;
}

void send_pulse (int rcpt)
{
	StoCmessage message;
	memset (&message, 0, sizeof (message));

	message.type = StoC_PULSE;
	send_StoCmessage (&message, rcpt);
}

/* Receive client to server message */
void receive_CtoSmessage (CtoSmessage* msg, int* sender)
{
	char buffer [BUFSIZE];
	char* readptr = buffer;
	fd_set set;
	struct timeval timeout;
	int sseq;
	int error;
	struct sockaddr_storage source;
	socklen_t source_size = sizeof(source);
	
tryagain:
	readptr = buffer;
	memset (buffer, 0, sizeof(buffer));
	memset (&timeout, 0, sizeof(timeout));
	FD_ZERO (&set);
	FD_SET (sockhdl, &set);
	
	error = select(sockhdl+1, &set, 0, 0, &timeout);
	if (error < 0) bailout ("s", "error on select socket");
	
	if (!FD_ISSET(sockhdl, &set)) {
		msg->type = CtoS_NOMSG;
		return;
	}

	error = recvfrom(sockhdl, buffer, sizeof(buffer), 0, (struct sockaddr*) &source, &source_size);
	if (error != sizeof(buffer)) bailout ("s", "error on receive from socket");
	
	msg->uid = strtol (readptr, &readptr, 10);
	*sender = client_by_uid (msg->uid);
	
	sseq = strtol (readptr, &readptr, 10);
	if ((sseq != 0) && (sender >= 0)) {
		if (sseq < seq_in[*sender]+1) {
			#if TOWERHACK_DEBUG
			fprintf (stderr, "[Debug] RECEIVE from %d: %s\n", *sender, buffer);
			#endif
			fprintf (stderr, "seq not in order, expected %d, got %d\n", seq_in[*sender]+1, sseq);
			goto tryagain;
		}
		seq_in[*sender] = sseq;//+1;
	}
	
	msg->type = parsetype (&readptr);
	#if TOWERHACK_DEBUG
	if (msg->type != CtoS_PULSE) fprintf (stderr, "[Debug] RECEIVE from %d: %s\n", *sender, buffer);
	#endif
	if ((*sender == -1) && (msg->type != CtoS_HELLO)) {
		sendto (sockhdl, "0 KICK\n", sizeof("0 KICK\n"), 0, (struct sockaddr*) &source, source_size); 
		#if TOWERHACK_DEBUG
		fprintf (stderr, "[Debug] Unknown client uid %d got rejected.\n", msg->uid);
		#endif
		goto tryagain;
	}
	 
	peer[*sender].disc_countdown = DISC_TIMELIMIT; // do not consider message type for this	
	
	int type, actor, start_time, loc_x, loc_y, loc_z;
	Direction dir;
	
	switch (msg->type) {
	
	case CtoS_PULSE:
		send_pulse (*sender);
		goto tryagain;
		
	case CtoS_HELLO:
		parsestring (msg->nickname, &readptr);
		insert_client (msg->nickname, msg->uid, &source, source_size);
		goto tryagain;
	
	case CtoS_REPEAT:
		msg->seq = strtol (readptr, &readptr, 10);
		for (int i = 0; i < MAX_REPEAT_AT_ONCE; i++) {
			if ((msg->seq % MSGLOG_SIZE) == (seq[*sender] % MSGLOG_SIZE)) break;
			send_StoCmessage_seq (&msglog [*sender][ msg->seq % MSGLOG_SIZE ], *sender, &msg->seq);
		}
		goto tryagain;
	
	case CtoS_CHAT:  /* (char* line) contains chat message to be sent to everyone */
		parsestring (msg->line, &readptr);
		break;
	
	case CtoS_LOBBY_ADDPLAYER:
		parsestring (msg->data.nickname, &readptr);
		msg->data.character = strtol (readptr, &readptr, 10);
		insert_player (*sender, msg->data.nickname, msg->data.character);
		goto tryagain;
	
	case CtoS_LOBBY_SETPLAYERDATA:
		msg->slot = strtol (readptr, &readptr, 10);
		parsestring (msg->data.nickname, &readptr);
		msg->data.character = strtol (readptr, &readptr, 10);
		break;
	
	case CtoS_LOBBY_REMOVEPLAYER:
		msg->slot = strtol (readptr, &readptr, 10);
		remove_player (msg->slot);
		goto tryagain;
	
	case CtoS_LOBBY_READY:
		break;
	
	case CtoS_LOBBY_NOTREADY:
		break;
	
	case CtoS_INGAME_ACTION:
		type = strtol (readptr, &readptr, 10);
		actor = strtol (readptr, &readptr, 10);
		start_time = strtol (readptr, &readptr, 10);
		loc_x = strtol (readptr, &readptr, 10);
		loc_y = strtol (readptr, &readptr, 10);
		loc_z = strtol (readptr, &readptr, 10);
		dir = strtol (readptr, &readptr, 10);
		
		int weapon;
		switch (type) {

		case ACTION_ATTACK_CLOSE:
			weapon = strtol (readptr, &readptr, 10);
			msg->action = action_alloc_closeattack (0, 0, weapon);
			break;
		
		case ACTION_ATTACK_RANGE:
			weapon = strtol (readptr, &readptr, 10);
			msg->action = action_alloc_rangeattack (0, 0, weapon);
			break;
			
		default:
			msg->action = no_action;
			break;
		
		}
		
		msg->action.type = type;
		msg->action.actor = actor;
		msg->action.start_time = start_time;
		msg->action.location.x = loc_x;
		msg->action.location.y = loc_y;
		msg->action.location.z = loc_z;
		msg->action.dir = dir;
		break;
				
	case CtoS_INGAME_SETINVENTORY:	
		msg->player = strtol (readptr, &readptr, 10);
		msg->inv.slot[0] = strtol (readptr, &readptr, 10);
		msg->inv.slot[1] = strtol (readptr, &readptr, 10);
		msg->inv.slot[2] = strtol (readptr, &readptr, 10);
		msg->inv.slot[3] = strtol (readptr, &readptr, 10);
		msg->inv.slot[4] = strtol (readptr, &readptr, 10);
		break;
	
	default:
		fprintf(stderr, "unknown message type\n");
		break;
	}
}
